package pl.edu.mimuw.irs.core.model.simulation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.model.analysis.History;
import pl.edu.mimuw.irs.core.model.analysis.Time;
import pl.edu.mimuw.irs.core.model.design.resource.Resource;
import pl.edu.mimuw.irs.core.util.Pair;

public class Calculations {
	
	public static final int CALCULATE_RESOURCES = 1;
	
	private static final int AVG_SIZE = 5;
	
	// *** Attributes

	private List<History> runs;
	private int minCost;
	private int maxCost;
	private int avgCost;
	private double devCost;
	private int lquCost;
	private int medCost;
	private int hquCost;
	private int minTime;
	private int maxTime;
	private int avgTime;
	private double devTime;
	private int lquTime;
	private int medTime;
	private int hquTime;
	private Integer[] avgCosts;
	private Integer[] avgTimes;
	private boolean calculateResources;
	private Map<Resource, Integer[]> resourcesAvgCosts;
	private Map<Resource, Integer[]> resourcesAvgTimes;
	public List<History> getRuns() {
		if (runs == null) runs = new ArrayList<History>();
		return runs;
	}
	public int getMinCost() {
		return minCost;
	}
	public int getMaxCost() {
		return maxCost;
	}
	public int getAvgCost() {
		return avgCost;
	}
	public double getDevCost() {
		return devCost;
	}
	public int getLquCost() {
		return lquCost;
	}
	public int getMedCost() {
		return medCost;
	}
	public int getHquCost() {
		return hquCost;
	}
	public int getMinTime() {
		return minTime;
	}
	public int getMaxTime() {
		return maxTime;
	}
	public int getAvgTime() {
		return avgTime;
	}
	public double getDevTime() {
		return devTime;
	}
	public int getLquTime() {
		return lquTime;
	}
	public int getMedTime() {
		return medTime;
	}
	public int getHquTime() {
		return hquTime;
	}
	public Integer[] getAvgCosts() {
		return avgCosts;
	}
	public Integer[] getAvgTimes() {
		return avgTimes;
	}
	public Map<Resource, Integer[]> getResourcesAvgCosts() {
		if (resourcesAvgCosts == null) resourcesAvgCosts = new LinkedHashMap<Resource, Integer[]>();
		return resourcesAvgCosts;
	}
	public Map<Resource, Integer[]> getResourcesAvgTimes() {
		if (resourcesAvgTimes == null) resourcesAvgTimes = new LinkedHashMap<Resource, Integer[]>();
		return resourcesAvgTimes;
	}
	
	
	// *** Constructors
	
	public Calculations(List<History> runs) {
		this(runs, AVG_SIZE, 0);
	}
	
	public Calculations(List<History> runs, int avgSize, int flags) {
		/* Safety precautions */
		avgSize = Math.min(runs.size(), avgSize);
		
		this.runs = runs;
		this.avgCosts = new Integer[avgSize];
		this.avgTimes = new Integer[avgSize];
		if ((flags & CALCULATE_RESOURCES) > 0) calculateResources = true;
		init();
	}
	
	// *** Methods
	
	public void init() {
		
		int size = getRuns().size();
		
		if (size == 0) {
			minCost = 0;
			maxCost = 0;
			avgCost = 0;
			lquCost = 0;
			medCost = 0;
			hquCost = 0;
			minTime = 0;
			maxTime = 0;
			avgTime = 0;
			lquTime = 0;
			medTime = 0;
			hquTime = 0;
			for (int i = 0; i < avgCosts.length; i++) {
				avgCosts[i] = 0;
				avgTimes[i] = 0;
			}

			return;
		}
		
		int pos = 0;
		List<Pair<Integer, Map<Resource, Time>>> costs = new ArrayList<Pair<Integer, Map<Resource, Time>>>(size);
		List<Pair<Integer, Map<Resource, Time>>> times = new ArrayList<Pair<Integer, Map<Resource, Time>>>(size);
		
		minCost = Integer.MAX_VALUE;
		maxCost = Integer.MIN_VALUE;
		avgCost = 0;
		devCost = 0;
		lquCost = 0;
		medCost = 0;
		hquCost = 0;
		minTime = Integer.MAX_VALUE;
		maxTime = Integer.MIN_VALUE;
		avgTime = 0;
		devTime = 0;
		lquTime = 0;
		medTime = 0;
		hquTime = 0;
		for (int i = 0; i < avgCosts.length; i++) {
			avgCosts[i] = 0;
			avgTimes[i] = 0;
		}
		
		for (History history : getRuns()) {
			int totalUsageCost = history.getTotalUsageCost();
			if (maxCost <= totalUsageCost) maxCost = totalUsageCost;
			if (minCost >= totalUsageCost) minCost = totalUsageCost;
			avgCost += totalUsageCost;
			history.getUsage();
			costs.add(new Pair<Integer, Map<Resource, Time>>(totalUsageCost, history.getUsage()));
			
			int totalTime = history.getTimestamp().getValue();
			if (maxTime <= totalTime) maxTime = totalTime;
			if (minTime >= totalTime) minTime = totalTime;
			avgTime += totalTime;
			times.add(new Pair<Integer, Map<Resource, Time>>(totalTime, history.getUsage()));
			
			pos++;
		}

		ByKeyComparator comparator = new ByKeyComparator();
		Collections.sort(costs, comparator);
		Collections.sort(times, comparator);
		
		avgCost /= size;
		avgTime /= size;
		lquCost = costs.get(size / 4).getKey();
		lquTime = times.get(size / 4).getKey();
		medCost = costs.get(size / 2).getKey();
		medTime = times.get(size / 2).getKey();
		hquCost = costs.get(size * 3 / 4).getKey();
		hquTime = times.get(size * 3 / 4).getKey();

		/* More loops required for deviation and local avgs */
		
		pos = 0;
		int minorCounter = 0;
		int majorCounter = 0;
		for (int i = 0; i < size; i++) {
			pos++;
			int totalUsageCost = costs.get(i).getKey();
			int totalTime = times.get(i).getKey();
			
			minorCounter++;

			/* Add values to deviation^2 quantifier */
			devCost += (totalUsageCost - avgCost) * (totalUsageCost - avgCost);
			devTime += (totalTime - avgTime) * (totalTime - avgTime);

			/* Add values to averages */
			avgCosts[majorCounter] += totalUsageCost;
			avgTimes[majorCounter] += totalTime;
			
			/* Add time and cost for resources */
			if (calculateResources) {
				Map<Resource, Time> costsUsage = costs.get(i).getValue();
				Map<Resource, Time> timesUsage = times.get(i).getValue();
				
				for (Entry<Resource, Time> entry : costsUsage.entrySet()) {
					Integer[] costsTable = getResourcesAvgCosts().get(entry.getKey());
					if (costsTable == null) {
						costsTable = new Integer[avgCosts.length];
						Arrays.fill(costsTable, 0);
						getResourcesAvgCosts().put(entry.getKey(), costsTable);
					}
					costsTable[majorCounter] += entry.getValue().getValue();
				}
				
				for (Entry<Resource, Time> entry : timesUsage.entrySet()) {
					Integer[] timesTable = getResourcesAvgTimes().get(entry.getKey());
					if (timesTable == null) {
						timesTable = new Integer[avgTimes.length];
						Arrays.fill(timesTable, 0);
						getResourcesAvgTimes().put(entry.getKey(), timesTable);
					}
					timesTable[majorCounter] += entry.getValue().getValue();
				}
			}
			
			// Works only for runs count >= avgCosts.length
			if (pos == size * (majorCounter + 1) / avgCosts.length) {
				avgCosts[majorCounter] /= minorCounter;
				avgTimes[majorCounter] /= minorCounter;
				
				if (calculateResources) {
					for (Entry<Resource, Integer[]> entry : getResourcesAvgCosts().entrySet()) {
						entry.getValue()[majorCounter] /= minorCounter;
					}
					for (Entry<Resource, Integer[]> entry : getResourcesAvgTimes().entrySet()) {
						entry.getValue()[majorCounter] /= minorCounter;
					}
				}
				
				minorCounter = 0;
				majorCounter++;
			}
		}

		for (Entry<Resource, Integer[]> entry : getResourcesAvgCosts().entrySet()) {
			Integer[] costsArray = entry.getValue();
			for (int i = 0; i < costsArray.length; i++) {
				costsArray[i] *= entry.getKey().getUnitCost();
			}
		}
		
		devCost = Math.sqrt(devCost / size);
		devTime = Math.sqrt(devTime / size);
	}
	
	@Override
	public String toString() {
//		return 
//			"NC=" + minCost + ";" + 
//			"XC=" + maxCost + ";" + 
//			"GC=" + avgCost + ";" + 
//			"DC=" + medCost + ";" + 
//			"NT=" + minTime + ";" + 
//			"XT=" + maxTime + ";" + 
//			"GT=" + avgTime + ";" + 
//			"DT=" + medTime + ";";
		String devCostString = Double.toString(devCost);
		String devTimeString = Double.toString(devTime);
		int devCostIndex = devCostString.indexOf('.');
		int devTimeIndex = devTimeString.indexOf('.');
		
		if (devCostIndex >= 0) devCostString = devCostString.substring(0, Math.min(devCostIndex + 3, devCostString.length()));
		if (devTimeIndex >= 0) devTimeString = devTimeString.substring(0, Math.min(devTimeIndex + 3, devTimeString.length()));
		
//		return
//			"Min=" + minCost + "/" + minTime + ";" +
//			"Max=" + maxCost + "/" + maxTime + ";" +
//			"Avg=" + avgCost + "/" + avgTime + ";" +
//			"Dev=" + devCostString + "/" + devTimeString + ";" +
//			"LQu=" + lquCost + "/" + lquTime + ";" +
//			"Med=" + medCost + "/" + medTime + ";" +
//			"HQu=" + hquCost + "/" + hquTime + ";";
			
//		return
//		"Cost: [" + avgCost + "~" + devCostString + "] " + minCost + "|" + lquCost + "|" + medCost + "|" + hquCost + "|" + maxCost + "  " +
//		"Time: [" + avgTime + "~" + devTimeString + "] " + minTime + "|" + lquTime + "|" + medTime + "|" + hquTime + "|" + maxTime + "  ";
		StringBuilder sb = new StringBuilder();
		sb.append("Cost: [" + avgCost + "~" + devCostString + "] " + avgCosts[0]);
		for (int i = 1; i < avgCosts.length; i++) sb.append("|" + avgCosts[i]);
		sb.append("  ");
		sb.append("Time: [" + avgTime + "~" + devTimeString + "] " + avgTimes[0]);
		for (int i = 1; i < avgTimes.length; i++) sb.append("|" + avgTimes[i]);
		sb.append("  ");
		return sb.toString();
	}

	private class ByKeyComparator implements Comparator<Pair<Integer, ?>> {

		@Override
		public int compare(Pair<Integer, ?> o1, Pair<Integer, ?> o2) {
			return o1.getKey().compareTo(o2.getKey());
		}
	}
}
